#include "window.hpp"

#include <hex.hpp>
#include <hex/api/content_registry.hpp>

#include <iostream>
#include <numeric>

#include <imgui.h>
#include <imgui_internal.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include <imgui_freetype.h>
#include <imgui_imhex_extensions.h>

#include "helpers/plugin_handler.hpp"

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#if defined(OS_WINDOWS)
    #include <windows.h>
#endif

namespace hex {

    constexpr auto MenuBarItems = { "File", "Edit", "View", "Help" };

    void *ImHexSettingsHandler_ReadOpenFn(ImGuiContext *ctx, ImGuiSettingsHandler *, const char *) {
        return ctx; // Unused, but the return value has to be non-null
    }

    void ImHexSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler *handler, void *, const char* line) {
        for (auto &view : ContentRegistry::Views::getEntries()) {
            std::string format = std::string(view->getName()) + "=%d";
            sscanf(line, format.c_str(), &view->getWindowOpenState());
        }
    }

    void ImHexSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler *handler, ImGuiTextBuffer *buf) {
        buf->reserve(buf->size() + 0x20); // Ballpark reserve

        buf->appendf("[%s][General]\n", handler->TypeName);

        for (auto &view : ContentRegistry::Views::getEntries()) {
            buf->appendf("%s=%d\n", view->getName().data(), view->getWindowOpenState());
        }

        buf->append("\n");
    }

    Window::Window(int &argc, char **&argv) {
        hex::SharedData::mainArgc = argc;
        hex::SharedData::mainArgv = argv;

        // Try to attach to a currently open console on Windows if available
        #if defined(OS_WINDOWS)
            if (AttachConsole(ATTACH_PARENT_PROCESS)) {
                HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
                if (hStdOut != INVALID_HANDLE_VALUE) {
                    freopen("CONOUT$", "w", stdout);
                    freopen("CONERR$", "w", stderr);
                    setvbuf(stdout, nullptr, _IONBF, 0);
                    setvbuf(stderr, nullptr, _IONBF, 0);
                }
            }
        #endif

        ContentRegistry::Settings::load();
        View::postEvent(Events::SettingsChanged);

        this->initGLFW();
        this->initImGui();
    }

    Window::~Window() {
        this->deinitImGui();
        this->deinitGLFW();
        ContentRegistry::Settings::store();

        for (auto &view : ContentRegistry::Views::getEntries())
            delete view;
        ContentRegistry::Views::getEntries().clear();

        this->deinitPlugins();
    }

    void Window::loop() {
        while (!glfwWindowShouldClose(this->m_window)) {
            this->frameBegin();

            for (const auto &call : View::getDeferedCalls())
                call();
            View::getDeferedCalls().clear();

            for (auto &view : ContentRegistry::Views::getEntries()) {
                if (!view->isAvailable() || !view->getWindowOpenState())
                    continue;

                auto minSize = view->getMinSize();
                minSize.x *= this->m_globalScale;
                minSize.y *= this->m_globalScale;

                ImGui::SetNextWindowSizeConstraints(minSize, view->getMaxSize());
                view->drawContent();
            }

            View::drawCommonInterfaces();

            #ifdef DEBUG
                if (this->m_demoWindowOpen)
                    ImGui::ShowDemoWindow(&this->m_demoWindowOpen);
            #endif

            this->frameEnd();
        }
    }

    bool Window::setFont(const std::filesystem::path &path) {
        if (!std::filesystem::exists(path))
            return false;

        auto &io = ImGui::GetIO();

        // If we have a custom font, then rescaling is unnecessary and will make it blurry
        io.FontGlobalScale = 1.0f;

        // Load font data & build atlas
        std::uint8_t *px;
        int w, h;
        io.Fonts->AddFontFromFileTTF(path.string().c_str(), std::floor(14.0f * this->m_fontScale)); // Needs conversion to char for Windows
        ImGuiFreeType::BuildFontAtlas(io.Fonts, ImGuiFreeType::Monochrome);
        io.Fonts->GetTexDataAsRGBA32(&px, &w, &h);

        // Create new font atlas
        GLuint tex;
        glGenTextures(1, &tex);
        glBindTexture(GL_TEXTURE_2D, tex);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA8, GL_UNSIGNED_INT, px);
        io.Fonts->SetTexID(reinterpret_cast<ImTextureID>(tex));

        return true;
    }

    void Window::frameBegin() {
        glfwPollEvents();

        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();

        ImGuiViewport* viewport = ImGui::GetMainViewport();
        ImGui::SetNextWindowPos(viewport->GetWorkPos());
        ImGui::SetNextWindowSize(viewport->GetWorkSize());
        ImGui::SetNextWindowViewport(viewport->ID);
        ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
        ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);

        ImGuiWindowFlags windowFlags = ImGuiWindowFlags_MenuBar     | ImGuiWindowFlags_NoDocking
                                     | ImGuiWindowFlags_NoTitleBar  | ImGuiWindowFlags_NoCollapse
                                     | ImGuiWindowFlags_NoMove      | ImGuiWindowFlags_NoResize
                                     | ImGuiWindowFlags_NoNavFocus  | ImGuiWindowFlags_NoBringToFrontOnFocus;

        ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;

        if (ImGui::Begin("DockSpace", nullptr, windowFlags)) {
            ImGui::PopStyleVar(2);
            ImGui::DockSpace(ImGui::GetID("MainDock"), ImVec2(0.0f, 0.0f));

            if (ImGui::BeginMenuBar()) {

                for (auto menu : MenuBarItems)
                    if (ImGui::BeginMenu(menu)) ImGui::EndMenu();

                if (ImGui::BeginMenu("View")) {
                    for (auto &view : ContentRegistry::Views::getEntries()) {
                        if (view->isAvailable() && view->hasViewMenuItemEntry())
                            ImGui::MenuItem((std::string(view->getName()) + " View").c_str(), "", &view->getWindowOpenState());
                    }
                    ImGui::EndMenu();
                }

                for (auto &view : ContentRegistry::Views::getEntries()) {
                    view->drawMenu();
                }

                if (ImGui::BeginMenu("View")) {
                    ImGui::Separator();
                    ImGui::MenuItem("Display FPS", "", &this->m_fpsVisible);
                    #ifdef DEBUG
                        ImGui::MenuItem("Demo View", "", &this->m_demoWindowOpen);
                    #endif
                    ImGui::EndMenu();
                }

                if (this->m_fpsVisible) {
                    char buffer[0x20];
                    snprintf(buffer, 0x20, "%.1f FPS", ImGui::GetIO().Framerate);

                    ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::GetFontSize() * strlen(buffer) + 20);
                    ImGui::TextUnformatted(buffer);
                }

                ImGui::EndMenuBar();
            }

            if (auto &[key, mods] = Window::s_currShortcut; key != -1) {
                for (auto &view : ContentRegistry::Views::getEntries()) {
                    if (view->getWindowOpenState()) {
                        if (view->handleShortcut(key, mods))
                            break;
                    }
                }

                Window::s_currShortcut = { -1, -1 };
            }

            bool anyViewOpen = false;
            for (auto &view : ContentRegistry::Views::getEntries())
                anyViewOpen = anyViewOpen || (view->getWindowOpenState() && view->isAvailable());

            if (!anyViewOpen) {
                char title[256];
                ImFormatString(title, IM_ARRAYSIZE(title), "%s/DockSpace_%08X", ImGui::GetCurrentWindow()->Name, ImGui::GetID("MainDock"));
                if (ImGui::Begin(title)) {
                    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10 * this->m_globalScale, 10 * this->m_globalScale));
                    if (ImGui::BeginChild("Welcome Screen", ImVec2(0, 0), false, ImGuiWindowFlags_AlwaysUseWindowPadding | ImGuiWindowFlags_NoDecoration)) {
                        this->drawWelcomeScreen();
                    }
                    ImGui::EndChild();
                    ImGui::PopStyleVar();
                }
                ImGui::End();
            }

        }
        ImGui::End();
    }

    void Window::frameEnd() {
        ImGui::Render();

        int displayWidth, displayHeight;
        glfwGetFramebufferSize(this->m_window, &displayWidth, &displayHeight);
        glViewport(0, 0, displayWidth, displayHeight);
        glClearColor(0.45f, 0.55f, 0.60f, 1.00f);
        glClear(GL_COLOR_BUFFER_BIT);
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

        GLFWwindow* backup_current_context = glfwGetCurrentContext();
        ImGui::UpdatePlatformWindows();
        ImGui::RenderPlatformWindowsDefault();
        glfwMakeContextCurrent(backup_current_context);

        glfwSwapBuffers(this->m_window);
    }

    void Window::drawWelcomeScreen() {
        ImGui::UnderlinedText("Welcome to ImHex!", ImGui::GetStyleColorVec4(ImGuiCol_HeaderActive));

        ImGui::NewLine();

        auto availableSpace = ImGui::GetContentRegionAvail();

        ImGui::Indent();
        if (ImGui::BeginTable("Welcome Left", 1, ImGuiTableFlags_NoBordersInBody, ImVec2(availableSpace.x / 2, availableSpace.y))) {
            ImGui::TableNextRow(ImGuiTableRowFlags_None, 100);
            ImGui::TableNextColumn();
            ImGui::Text("Start");
            {
                if (ImGui::BulletHyperlink("Open File"))
                    EventManager::post(Events::OpenWindow, "Open File");
            }
            ImGui::TableNextRow(ImGuiTableRowFlags_None, 100);
            ImGui::TableNextColumn();
            ImGui::Text("Recent");
            {

            }
            ImGui::TableNextRow(ImGuiTableRowFlags_None, 100);
            ImGui::TableNextColumn();
            ImGui::Text("Help");
            {
                if (ImGui::BulletHyperlink("GitHub Repository")) hex::openWebpage("https://github.com/WerWolv/ImHex");
                if (ImGui::BulletHyperlink("Get help")) hex::openWebpage("https://github.com/WerWolv/ImHex/discussions/categories/get-help");
            }

            ImGui::EndTable();
        }
        ImGui::SameLine();
        if (ImGui::BeginTable("Welcome Right", 1, ImGuiTableFlags_NoBordersInBody, ImVec2(availableSpace.x / 2, availableSpace.y))) {
            ImGui::TableNextRow(ImGuiTableRowFlags_None, 100);
            ImGui::TableNextColumn();
            ImGui::Text("Customize");
            {
                if (ImGui::DescriptionButton("Settings", "Change preferences of ImHex", ImVec2(ImGui::GetContentRegionAvail().x * 0.8f, 0)))
                    EventManager::post(Events::OpenWindow, "Preferences");
            }
            ImGui::TableNextRow(ImGuiTableRowFlags_None, 100);
            ImGui::TableNextColumn();
            ImGui::Text("Learn");
            {
                if (ImGui::DescriptionButton("Pattern Language Documentation", "Learn how to write ImHex patterns with our extensive documentation", ImVec2(ImGui::GetContentRegionAvail().x * 0.8, 0)))
                    hex::openWebpage("https://github.com/WerWolv/ImHex/wiki/Pattern-Language-Guide");
                if (ImGui::DescriptionButton("Plugins API", "Extend ImHex with additional features using plugins", ImVec2(ImGui::GetContentRegionAvail().x * 0.8, 0)))
                    hex::openWebpage("https://github.com/WerWolv/ImHex/wiki/Plugins-Development-Guide");
            }

            ImGui::EndTable();
        }
    }

     void Window::initGLFW() {
        glfwSetErrorCallback([](int error, const char* desc) {
            fprintf(stderr, "Glfw Error %d: %s\n", error, desc);
        });

        if (!glfwInit())
            throw std::runtime_error("Failed to initialize GLFW!");

        #ifdef __APPLE__
            glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
        #endif

        if (auto *monitor = glfwGetPrimaryMonitor(); monitor) {
            float xscale, yscale;
            glfwGetMonitorContentScale(monitor, &xscale, &yscale);

            // In case the horizontal and vertical scale are different, fall back on the average
            this->m_globalScale = this->m_fontScale = std::midpoint(xscale, yscale);
        }

        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);


        this->m_window = glfwCreateWindow(1280 * this->m_globalScale, 720 * this->m_globalScale, "ImHex", nullptr, nullptr);


        if (this->m_window == nullptr)
            throw std::runtime_error("Failed to create window!");

        glfwMakeContextCurrent(this->m_window);
        glfwSwapInterval(1);

         {
             int x = 0, y = 0;
             glfwGetWindowPos(this->m_window, &x, &y);
             SharedData::windowPos = ImVec2(x, y);
         }

         {
             int width = 0, height = 0;
             glfwGetWindowSize(this->m_window, &width, &height);
             SharedData::windowSize = ImVec2(width, height);
         }

         glfwSetWindowPosCallback(this->m_window, [](GLFWwindow *window, int x, int y) {
             SharedData::windowPos = ImVec2(x, y);
         });

        glfwSetWindowSizeCallback(this->m_window, [](GLFWwindow *window, int width, int height) {
            SharedData::windowSize = ImVec2(width, height);
        });

        glfwSetKeyCallback(this->m_window, [](GLFWwindow *window, int key, int scancode, int action, int mods) {
            if (action == GLFW_PRESS) {
                Window::s_currShortcut = { key, mods };
                auto &io = ImGui::GetIO();
                io.KeysDown[key] = true;
                io.KeyCtrl  = (mods & GLFW_MOD_CONTROL) != 0;
                io.KeyShift = (mods & GLFW_MOD_SHIFT) != 0;
                io.KeyAlt   = (mods & GLFW_MOD_ALT) != 0;
            }
            else if (action == GLFW_RELEASE) {
                auto &io = ImGui::GetIO();
                io.KeysDown[key] = false;
                io.KeyCtrl  = (mods & GLFW_MOD_CONTROL) != 0;
                io.KeyShift = (mods & GLFW_MOD_SHIFT) != 0;
                io.KeyAlt   = (mods & GLFW_MOD_ALT) != 0;
            }
        });

        glfwSetDropCallback(this->m_window, [](GLFWwindow *window, int count, const char **paths) {
            if (count != 1)
                return;

            View::postEvent(Events::FileDropped, paths[0]);
        });

        glfwSetWindowCloseCallback(this->m_window, [](GLFWwindow *window) {
            View::postEvent(Events::WindowClosing, window);
        });


        glfwSetWindowSizeLimits(this->m_window, 720, 480, GLFW_DONT_CARE, GLFW_DONT_CARE);

        if (gladLoadGL() == 0)
            throw std::runtime_error("Failed to initialize OpenGL loader!");
    }

    void Window::initImGui() {
        IMGUI_CHECKVERSION();
        auto *ctx = ImGui::CreateContext();
        GImGui = ctx;

        ImGuiIO& io = ImGui::GetIO();
        ImGuiStyle& style = ImGui::GetStyle();

        io.ConfigFlags |= ImGuiConfigFlags_DockingEnable | ImGuiConfigFlags_ViewportsEnable | ImGuiConfigFlags_NavEnableKeyboard;
        io.ConfigViewportsNoTaskBarIcon = true;
        io.KeyMap[ImGuiKey_Tab]         = GLFW_KEY_TAB;
        io.KeyMap[ImGuiKey_LeftArrow]   = GLFW_KEY_LEFT;
        io.KeyMap[ImGuiKey_RightArrow]  = GLFW_KEY_RIGHT;
        io.KeyMap[ImGuiKey_UpArrow]     = GLFW_KEY_UP;
        io.KeyMap[ImGuiKey_DownArrow]   = GLFW_KEY_DOWN;
        io.KeyMap[ImGuiKey_PageUp]      = GLFW_KEY_PAGE_UP;
        io.KeyMap[ImGuiKey_PageDown]    = GLFW_KEY_PAGE_DOWN;
        io.KeyMap[ImGuiKey_Home]        = GLFW_KEY_HOME;
        io.KeyMap[ImGuiKey_End]         = GLFW_KEY_END;
        io.KeyMap[ImGuiKey_Insert]      = GLFW_KEY_INSERT;
        io.KeyMap[ImGuiKey_Delete]      = GLFW_KEY_DELETE;
        io.KeyMap[ImGuiKey_Backspace]   = GLFW_KEY_BACKSPACE;
        io.KeyMap[ImGuiKey_Space]       = GLFW_KEY_SPACE;
        io.KeyMap[ImGuiKey_Enter]       = GLFW_KEY_ENTER;
        io.KeyMap[ImGuiKey_Escape]      = GLFW_KEY_ESCAPE;
        io.KeyMap[ImGuiKey_KeyPadEnter] = GLFW_KEY_KP_ENTER;
        io.KeyMap[ImGuiKey_A]           = GLFW_KEY_A;
        io.KeyMap[ImGuiKey_C]           = GLFW_KEY_C;
        io.KeyMap[ImGuiKey_V]           = GLFW_KEY_V;
        io.KeyMap[ImGuiKey_X]           = GLFW_KEY_X;
        io.KeyMap[ImGuiKey_Y]           = GLFW_KEY_Y;
        io.KeyMap[ImGuiKey_Z]           = GLFW_KEY_Z;

        if (this->m_globalScale != 0.0f)
            style.ScaleAllSizes(this->m_globalScale);

        #if defined(OS_WINDOWS)
            std::filesystem::path resourcePath = std::filesystem::path((SharedData::mainArgv)[0]).parent_path();
        #elif defined(OS_LINUX) || defined(OS_MACOS)
            std::filesystem::path resourcePath = "/usr/share/ImHex";
        #else
            std::filesystem::path resourcePath = "";
            #warning "Unsupported OS for custom font support"
        #endif

        if (!resourcePath.empty() && this->setFont(resourcePath / "font.ttf")) {

        }
        else if ((this->m_fontScale != 0.0f) && (this->m_fontScale != 1.0f)) {
            io.Fonts->Clear();

            ImFontConfig cfg;
            cfg.OversampleH = cfg.OversampleV = 1, cfg.PixelSnapH = true;
            cfg.SizePixels = 13.0f * this->m_fontScale;
            io.Fonts->AddFontDefault(&cfg);
        }

        style.WindowMenuButtonPosition = ImGuiDir_None;
        style.IndentSpacing = 10.0F;

        // Install custom settings handler
        ImGuiSettingsHandler handler;
        handler.TypeName = "ImHex";
        handler.TypeHash = ImHashStr("ImHex");
        handler.ReadOpenFn = ImHexSettingsHandler_ReadOpenFn;
        handler.ReadLineFn = ImHexSettingsHandler_ReadLine;
        handler.WriteAllFn = ImHexSettingsHandler_WriteAll;
        handler.UserData   = this;
        ctx->SettingsHandlers.push_back(handler);

        ImGui::StyleColorsDark();

        ImGui_ImplGlfw_InitForOpenGL(this->m_window, true);
        ImGui_ImplOpenGL3_Init("#version 150");
    }

    void Window::initPlugins() {
        try {
            auto pluginFolderPath = std::filesystem::path((SharedData::mainArgv)[0]).parent_path() / "plugins";
            PluginHandler::load(pluginFolderPath.string());
        } catch (std::runtime_error &e) { return; }

        for (const auto &plugin : PluginHandler::getPlugins()) {
            plugin.initializePlugin();
        }
    }

    void Window::deinitGLFW() {
        glfwDestroyWindow(this->m_window);
        glfwTerminate();
    }

    void Window::deinitImGui() {
        ImGui_ImplOpenGL3_Shutdown();
        ImGui_ImplGlfw_Shutdown();
        ImGui::DestroyContext();
    }

    void Window::deinitPlugins() {
        PluginHandler::unload();
    }

}